﻿#region Using Directives

using System;
using System.Data;
using System.Reflection;
using GeekRule.ORM.Entities;

#endregion

namespace GeekRule.ORM.DataMapping
{
    /// <summary>
    /// Maps data source results to strongly-typed entities
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class EntityDataMapper<T> where T : IEntity
    {
        #region Private Members

        private EntityMapping _entityMapping;

        #endregion

        #region Public Methods

        public T MapData(T entity, IDataReader reader)
        {
            Type entityType = entity.GetType();
            bool mappingIsCached = false;
            if (DataMappingCache.Instance.EntityMappings.ContainsKey(entityType))
            {
                _entityMapping = DataMappingCache.Instance.EntityMappings[entityType];
                mappingIsCached = true;
            }
            else
            {
                _entityMapping = new EntityMapping(entityType);
            }

            for (int i = 0; i < reader.FieldCount; i++)
            {
                string field = reader.GetName(i);
                Type type = reader.GetValue(i).GetType();
                SetProperty(entity, entityType, field, reader.GetValue(i), mappingIsCached);
            }

            if (!mappingIsCached)
            {
                DataMappingCache.Instance.EntityMappings.Add(entityType, _entityMapping);
            }

            return entity;
        }

        #endregion

        #region Private Helpers

        /// <summary>
        /// Obtain the custom class attribute of the given type
        /// </summary>
        /// <typeparam name="TAttribute"></typeparam>
        /// <returns></returns>
        private TAttribute GetClassAttribute<TAttribute>(IEntity entity) where TAttribute : Attribute
        {
            Type type = entity.GetType();
            if (type.IsDefined(typeof(TAttribute), true))
            {
                return Attribute.GetCustomAttribute(type, typeof (TAttribute)) as TAttribute;
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Performs data type conversion, when applicable, and returns the value
        /// </summary>
        /// <param name="propertyType"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        private object GetValue(Type propertyType, object value)
        {
            // TODO: need to research and implement additional mappings
            if (propertyType == typeof(Guid) && value is byte[]) // Guid stored as byte[]
            {
                return new Guid(value as byte[]);
            }
            else if (propertyType == typeof(bool) && value is UInt64) // MySql stores bit as UInt64
            {
                return value.Equals(1);
            }
            else if (propertyType == typeof(char[]) && value is string) // string-to-char[] mapping
            {
                return Convert.ToString(value).ToCharArray();
            }
            else
            {
                return value;
            }
        }
        /// <summary>
        /// Obtain a custom property attribute of a given type
        /// </summary>
        /// <typeparam name="TAttribute"></typeparam>
        /// <param name="propertyInfo"></param>
        /// <returns></returns>
        private TAttribute GetPropertyAttribute<TAttribute>(PropertyInfo propertyInfo) where TAttribute : class
        {
            if (Attribute.IsDefined(propertyInfo, typeof(TAttribute), true))
            {
                return Attribute.GetCustomAttribute(propertyInfo, typeof(TAttribute)) as TAttribute;
            }
            else
            {
                return default(TAttribute);
            }
        }

        /// <summary>
        /// Set an entity property value
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="entityType"></param>
        /// <param name="propertyName"></param>
        /// <param name="value"></param>
        /// <param name="mappingIsCached"></param>
        private void SetProperty(IEntity entity, Type entityType, string propertyName, object value, bool mappingIsCached)
        {
            PropertyInfo propertyInfo;
            if (mappingIsCached)
            {
                propertyInfo = _entityMapping.FieldMapping[propertyName];
            }
            else
            {
                string name = propertyName;
                if (propertyName.Contains("_")) name = name.Replace("_", string.Empty);
                if (propertyName.Contains(" ")) name = name.Replace(" ", string.Empty);

                propertyInfo = entityType.GetProperty(propertyName);

                if (propertyInfo == null)
                {
                    PropertyInfo[] properties = entityType.GetProperties();
                    for (int i = 0; i < properties.Length; i++)
                    {
                        if (properties[i].Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                        {
                            propertyInfo = properties[i];
                            break;
                        }
                        else
                        {
                            FieldMappingAttribute fieldMapping = GetPropertyAttribute<FieldMappingAttribute>(properties[i]);
                            if (fieldMapping != null && fieldMapping.FieldName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                            {
                                propertyInfo = properties[i];
                                break;
                            }
                        }
                    }
                }

                if (propertyInfo == null)
                {
                    DataMappingAttribute dataMapping = GetClassAttribute<DataMappingAttribute>(entity);
                    if (dataMapping == null || dataMapping.DataMappingMode == DataMappingMode.Strict)
                    {
                        throw new ArgumentException(propertyName + " property not found");
                    }
                    else if (dataMapping.DataMappingMode == DataMappingMode.StoreMissingFields)
                    {
                        entity.UnmappedFields.Add(propertyName, value);
                        return;
                    }
                    else
                    {
                        return; // DataMappingMode.IgnoreMissingFields
                    }
                }
            }

            if (!mappingIsCached)
            {
                _entityMapping.FieldMapping.Add(propertyName, propertyInfo);
                _entityMapping.PropertyMapping.Add(propertyInfo, propertyName);
            }

            Type propertyType = propertyInfo.PropertyType;
            if (value != DBNull.Value)
            {
                propertyInfo.SetValue(entity, GetValue(propertyType, value), null);
                return;
            }

            if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                propertyInfo.SetValue(entity, null, null);
            }
            else
            {
                throw new ApplicationException("Unexpected condition in SetProperty(): property value not set");
            }
        }

        #endregion
    }
}
